对 iOS 开发中的函数式编程的一点探索

iOS 的开发中,运用最广的 OC 语言是典型的面向对象语言,所以很多 iOS 的同学可能更习惯于 OOP 编程模式。

但是随着 Swift 的推出和应用,更加丰富的编程模式开始被讨论。虽然 Swift 被定义为面向协议语言,但是其中仍然有很多函数式思想,在这里我谈一下对 iOS 开发中,函数式编程的一点理解。

如何理解 FP

函数式编程,顾名思义,其思想更接近于数学计算。

函数式编程是一种抽象度很高的编程方式,其中一个很重要的概念就是纯函数,即:对于一个函数来说,只要输入是确定的,那么输入就是确定的,这种函数是没有副作用的。

Swift 中的 FP 思想

函数式编程语言并不多,但是函数式编程思想却运用的很广泛。这里列出几个 Swift 中比较常用的数组变换操作函数。

比如在 Swift 中操作数组,这里有一个整型数组:

1
var array = [1, 2, 3, 4, 5]

首先实现数据各元素 +1,可能写起来是这个样子的:

1
2
3
4
5
6
7
func increment(array: [Int]) -> [Int] {
var result: [Int] = []
for x in array {
result.append(x + 1)
}
return result
}

那如果又想实现数据各元素 *2 呢,可能又会有这样一份代码:

1
2
3
4
5
6
7
func double(array: [Int]) -> [Int] {
var result: [Int] = []
for x in array {
result.append(x * 2)
}
return result
}

上述两份代码相似度已经达到不能容忍的状态,那么我们可以考虑将共同的提取出来,将差异从外界接收,也就是将对数组的转换操作,作为一个参数传入:

1
2
3
4
5
6
7
func compute(array: [Int], transform:(Int) -> (Int)) -> [Int] {
var result: [Int] = []
for x in array {
result.append(transform(x))
}
return result
}

这样我们就可以接收所有对整型数组的操作,并且返回对应的 result 了。

但是这样的局限性仍然很大,只能处理整型数组,为了更好的兼容其他类型,在这个基础上,可以引入泛型:

1
2
3
4
5
6
7
func genericCompute<Element, T>(array: [Element], transform:(Element) -> T) -> [T] {
var result: [T] = []
for x in array {
result.append(transform(x))
}
return result
}

至此基本上可以处理各种类型的数组,并且可以得到不同类型数组的返回值。事实上,Swift 中也确实是这样做的,Array 有对应的 map 函数,可以实现上述功能。而这种功能就是纯数组变换的,不参杂对外界对任何影响,只要输入确定,输出也一定确定。

同样的,Swift 中也提供了其他 Array 的变换函数。

filter 函数可以用来做过滤操作:

1
2
3
4
5
6
7
8
9
10
//  filter 的大概实现:
extension Array {
func filter(_ includeElement: (Element) -> Bool) -> [Element] {
var result: [Element] = []
for x in self where includeElement(x) {
result.append(x)
}
return result
}
}

而针对上述操作,可以把它们再归纳为:给定一个初始值以及变换函数,遍历更新结果的一个过程。因此,将 赋给 result 变量的初始值,和用于在每一次循环中更新 result 的函数 进行抽象,便可得到 reduce 函数:

1
2
3
4
5
6
7
8
9
10
//  reduce 的大概实现:
extension Array {
func reduce<T>(_ initial: T, combine: (T, Element) -> T) -> T {
var result = initial
for x in self {
result = combine(result, x)
}
return result
}
}

Swift 中对于以上几种函数已经封装好,我们可以随便调用对 Array 做想要的变换操作了。比起专门抽出一段来处理 Array,这种直接传入函数的调用方式更佳简洁,也更容易理解。

经典 OC 的 FRP 开源库:RAC 中的 FP 思想

iOS 中更多接触的是面向对象编程,但经典开源库 ReactiveCocoa 的 OC 版本中展示了一套在 OC 中运用的响应式函数式编程模式。我们通常对 RAC 的响应式讨论的比较多, 而 RAC 中的核心函数 bind,是函数式编程中 Monad 概念的一种运用。

关于函数式编程中的 Functor、Applicative 和 Monad 概念这里不再展开,想了解的可以看一下这篇博客

写一段简单的测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void bindForTest() {
RACSignal *signal = [RACSignal createSignal: ^RACDisposable *(id<RACSubscriber> subscriber) {
[subscriber sendNext:@1];
[subscriber sendNext:@2];
[subscriber sendNext:@3];
[subscriber sendCompleted];
return [RACDisposable disposableWithBlock:^{
NSLog(@"signal dispose");
}];
}];

RACSignal *bindSignal = [signal bind:^RACStreamBindBlock {
return ^RACSignal *(NSNumber *value, BOOL *stop) {
value = @(value.integerValue + 1);
return [RACSignal return:value];
};
}];

[bindSignal subscribeNext:^(id x) {
NSLog(@"subscribe value = %@", x);
}];
}

调用这个函数,运行可以看到控制台输出:

1
2
3
4
2018-07-30 13:52:52.686901+0800 FPTest[9654:689258] subscribe value = 2
2018-07-30 13:52:52.687415+0800 FPTest[9654:689258] subscribe value = 3
2018-07-30 13:52:52.687460+0800 FPTest[9654:689258] subscribe value = 4
2018-07-30 13:52:52.687494+0800 FPTest[9654:689258] signal dispose

RAC 的调用方式这里不展开,可以看到 bind 函数实现的结果同样是对一系列的值进行了变换,只不过这里的上下文可以理解为 RACSignal,我们可以才想到 RAC 的内部的 bind 函数实现方式,是将一个信号的每个值依次进行变换之后再输出,事实上也确实如此。

OC 中的日常应用

同样的,上述 Swift 的 Extention 中实现的功能,在 OC 中同样有场景需要,我们可以通过 block 的形式将变换函数传递进来,以实现类似的功能,比如 map 可以如下实现:

1
2
3
4
5
6
7
8
9
10
+ (NSArray *)map:(NSArray *)array block:(id(^)(id obj, NSInteger idx))block {
if (!block) {
return array;
}
NSMutableArray *result = [NSMutableArray array];
[array enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
[result addObject:block(obj, idx)];
}];
return result;
}

当然也可以放入 NSArray 的分类中,这样调用起来更加方便。

小结

如上所述,函数式编程思想实际上随处可见,在实际的开发中,我们应当在合适的场景下分别应用,争取可以以更轻巧简洁的方式解决问题。